# -*- encoding: utf-8 -*-

"""
------------------------------------------
@File       : life_game_v3.py
@Author     : maixiaochai
@Email      : maixiaochai@outlook.com
@CreatedOn  : 2021/7/21 0:09
------------------------------------------
    [ 生命游戏，Queue并发版 ]
    思路：
        在推进生命游戏时，提前创建数量固定的一组工作线程，
        令这组线程平行地处理当前这批 I/O任务，
        在处理完之后，等待下一批任务。
        如此，能减少很多资源消耗，也能减少频繁新建线程而带来的开销


    关系图：
        game_logic_thread有多个线程，以下为上一代产生下一代的推进过程，这个过程进行了5次,
        每次进行的时候in_queue和out_queue用的是全局的变量，中间 5个game_logic_thread线程也是一直存在的。
        或者可以这样理解，在生命游戏的5次推进中，整个流水线流水作业了5次。
        simulate_pipeline 是最辛苦的工人，堪称劳模，即使生产者也是消费者，干完前边工位的活儿，又去干后边工位的活儿。

                                         |--game_logic_thread_1 --|
                                         |  game_logic_thread_2   |
        simulate_pipeline -> in_queue_ ->|  game_logic_thread_3   | -> out_queue_ -> simulate_pipeline
                                         |  game_logic_thread_4   |
                                         |--game_logic_thread_5 --|
    优点（与v2相比）：
        1）使用内存少
        2）启动线程的开销小
        3）调试容易

    缺点:
        1）为了让代码更容易理解，必须使用 CloseableQueue和 StoppableWorker这样的类表示特质的类与工作线程，这会让程序变得复杂
        2）与并行度有关的参数（game_logic_thread函数的数量），必须依工作负载提前订好，而无法由系统根据工作量自动调整
        3）为了便于调试，需要在工作线程里手动捕获异常，并把它放入队列中，然后重新抛给主线程
        4）最严重的问题在需求发生变化的时候 —— 除了game_logic函数，现在 count_neighbors函数也需要做 I/O
"""
from queue import Queue
from threading import Thread

from life_game_v1 import ALIVE, Grid, count_neighbors, ColumnPrinter
from life_game_v2 import game_logic


class SimulationError(Exception):
    """自定义错误类型"""
    pass


class ClosableQueue(Queue):
    SENTINEL = object()

    def close(self):
        self.put(self.SENTINEL)

    def __iter__(self):
        while True:
            item = self.get()

            # 在 try finally结构中，如果try中遇到了return，是会先执行finally中的代码，再执行try中的return
            try:
                if item is self.SENTINEL:
                    return  # Cause the thread to exit
                yield item

            finally:
                # get 不会让queue中的未完成数-1，必须配合task_done,才能让queue中的未完成的任务数-1
                # 这里用finally的原因，即使是try下边return了，task_done也会执行，保证队列内被处理的任务标记为已处理
                self.task_done()


class StoppableWorker(Thread):
    def __init__(self, func, in_queue, out_queue, **kwargs):
        super().__init__(**kwargs)
        self.func = func
        self.in_queue = in_queue
        self.out_queue = out_queue

    def run(self):
        for item in self.in_queue:
            result = self.func(item)
            self.out_queue.put(result)


def game_logic_thread(item):
    y, x, state, neighbors = item

    try:
        next_state = game_logic(state, neighbors)
    except Exception as e:
        next_state = e

    return y, x, next_state


def simulate_pipeline(grid, in_queue, out_queue):
    for y in range(grid.height):
        for x in range(grid.width):
            state = grid.get(y, x)
            neighbors = count_neighbors(y, x, grid.get)
            in_queue.put((y, x, state, neighbors))  # fan out

    # join, 会在队列存在未完成任务时阻塞，等待队列无未完成任务，需要配合 task_done 使用
    in_queue.join()

    # A：这一步只是向out_queue中放入了一个 close的标记（而不是马上结束queue），当queue中按顺序识别到close标记的时候，会跳出阻塞式循环get
    out_queue.close()

    next_grid = Grid(grid.height, grid.width)

    # 这里的使用，在于CloseableQueue中设计地比较巧妙，在遍历到close标记时，退出阻塞式get，并且还给queue最后一个任务打了一个task_done,完美
    for item in out_queue:  # fan in
        y, x, next_state = item

        if isinstance(next_state, Exception):
            raise SimulationError(y, x) from next_state
        next_grid.set(y, x, next_state)

    return next_grid


def main():
    """
        主要业务逻辑
    """
    in_queue_ = ClosableQueue()
    out_queue_ = ClosableQueue()

    # 将 [ in_queue_ -> | game_logic_thread X 5 | -> out_queue_ ] 的半条流水线建立起来
    threads = []
    for _ in range(5):
        thread = StoppableWorker(game_logic_thread, in_queue_, out_queue_)
        thread.start()
        threads.append(thread)

    grid = Grid(5, 9)
    grid.set(0, 3, ALIVE)
    grid.set(1, 4, ALIVE)
    grid.set(2, 2, ALIVE)
    grid.set(2, 3, ALIVE)
    grid.set(2, 4, ALIVE)

    columns = ColumnPrinter()

    # 这里，simulate_pipeline 即充当了队列1之前的生产者，又充当了队列2的消费者,
    # 只不过是全部生产完全部东西放入in_queue_后，跑到最后(out_queue_)的后边"工位"，去充当消费者

    for i in range(5):
        columns.append(str(grid))
        grid = simulate_pipeline(grid, in_queue_, out_queue_)

    print(columns)

    for _ in threads:
        in_queue_.close()

    for thread in threads:
        thread.join()


def demo():
    """
        在 try finally结构中，如果try中遇到了return，是会先执行finally中的代码，再执行try中的return
        out:
            2
            3
    """
    try:
        return 3

    finally:
        print("2")


if __name__ == '__main__':
    main()
